package io.noties.markwon.base.image.load.comust

import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import io.noties.markwon.base.image.AsyncDrawable
import io.noties.markwon.base.image.DrawableUtils
import io.noties.markwon.base.image.load.AsyncDrawableLoader
import io.noties.markwon.base.image.size.ImageSizeResolver
import timber.log.Timber
import java.util.HashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future

class ADVAsyncDrawableLoader(private val imageLoadConfig: ImageLoadConfig = ImageLoadConfig(), val createDrawable:(AsyncDrawable)-> Drawable) : AsyncDrawableLoader {
    private val handler = Handler(Looper.getMainLooper())
    private val cache: MutableMap<AsyncDrawable, Future<*>?> = HashMap(3)
    override fun load(drawable: AsyncDrawable) {

        // this method must be called from main-thread only (thus synchronization can be skipped)

        // check for currently running tasks associated with provided drawable
        val future = cache[drawable]

        // if it's present -> proceed with new execution
        // as asyncDrawable is immutable, it won't have destination changed (so there is no need
        // to cancel any started tasks)
        if (future == null) {
            cache[drawable] =imageLoadConfig.executorService.submit {
                // @since 4.0.1 wrap in try-catch block and add error logging
                try {
                    setResult(drawable, createDrawable(drawable))
                } catch (t: Throwable) {
                    // @since 4.3.0 add error handling
                    val errorHandler = imageLoadConfig.errorHandler
                    if (errorHandler == null) {
                        // as before
                        Timber.e(t, "Error displaying : `" + drawable.destination + "`")
                    } else {
                        // just call `getDestination` without casts and checks
                        val errorDrawable = errorHandler.handleError(
                            drawable.destination,
                            t
                        )
                        if (errorDrawable != null) {
                            DrawableUtils.applyIntrinsicBoundsIfEmpty(errorDrawable)
                            setResult(drawable, errorDrawable)
                        }
                    }
                }
            }
        }
    }

    override fun cancel(drawable: AsyncDrawable) {

        // this method also must be called from main thread only
        val future = cache.remove(drawable)
        future?.cancel(true)

        // remove all callbacks (via runnable) and messages posted for this drawable
        handler.removeCallbacksAndMessages(drawable)
    }

    override fun placeholder(): Drawable? {
        return null
    }


    // @since 4.3.0
    private fun setResult(drawable: AsyncDrawable, result: Drawable) {
        // we must post to handler, but also have a way to identify the drawable
        // for which we are posting (in case of cancellation)
        handler.postAtTime({ // remove entry from cache (it will be present if task is not cancelled)
            if (cache.remove(drawable) != null && drawable.isAttached) {
                drawable.setResult(result)
            }
        }, drawable, SystemClock.uptimeMillis())
    }
}

open class ImageLoadConfig {

    // @since 4.3.0
    var blocksEnabled: Boolean=false
    var inlinesEnabled: Boolean=false

    // @since 4.3.0
    var errorHandler: ErrorHandler?=null
    var executorService: ExecutorService= Executors.newCachedThreadPool()
}

/**
 * @since 4.3.0
 */
interface ErrorHandler {
    /**
     * @param latex that caused the error
     * @param error occurred
     * @return (optional) error drawable that will be used instead (if drawable will have bounds
     * it will be used, if not intrinsic bounds will be set)
     */
    fun handleError(latex: String, error: Throwable): Drawable?
}

/**
 * @since 4.0.0
 */
interface PlaceholderProvider {
    fun providePlaceholder(): Drawable?
}

class InlineImageSizeResolver : ImageSizeResolver {
    override fun resolveImageSize(drawable: AsyncDrawable): Rect {

        // @since 4.4.0 resolve inline size (scale down if exceed available width)
        val imageBounds = drawable.resultDrawable!!.bounds
        val canvasWidth = drawable.lastKnownCanvasWidth
        val w = imageBounds.width()
        if (w > canvasWidth) {
            // here we must scale it down (keeping the ratio)
            val ratio = w.toFloat() / imageBounds.height()
            val h = (canvasWidth / ratio + .5f).toInt()
            return Rect(0, 0, canvasWidth, h)
        }
        return imageBounds
    }
}